var _proton = _p = {
    utils: {
        extend: function(c, p) {
            var f = function() { };
            f.prototype = p.prototype;
            c.prototype = new f();
            c.prototype.constructor = c;
            c.superclass = p.prototype;
        },
        bind: function(method, context) {
            var args = Array.prototype.slice.call(arguments, 2);
            return function() {
                var a = args.concat(Array.prototype.slice.call(arguments, 0));
                return method.apply(context, a);
            }
        },
        filterData: function(data, dataMask, proxyShortcut) {
            var result = {};
            if (!(data instanceof Object)) return data;
            //if (null == dataMask || null == data) return data;
            if (null == data) return data;
            if (data instanceof _proton.Model) {
                if (true === dataMask) {
                    var proxy = data.getProxy(proxyShortcut);
                    data = _proton.utils.filterData(data.data, proxy.dataMask, proxyShortcut);
                }   else {
                    data = data.data;
                }
            }

            for (var i in data) {
                var dataMaskItem = (null == dataMask || true == dataMask) ? true : dataMask[i];
                if (dataMaskItem && "function" != typeof(data[i])) {
                    if (data[i] != null && Array == data[i].constructor) {
                        result[i] = [];
                        for (var j=0; j<data[i].length; j++) {
                            result[i].push(_proton.utils.filterData(data[i][j], dataMaskItem, proxyShortcut));
                        }
                    } else if (data[i] != null /* && Object == dataMask[i].constructor */) {
                        result[i] = _proton.utils.filterData(data[i], dataMaskItem, proxyShortcut);
                    } else {
                        result[i] = data[i];
                    }
                }
            }
            return result;
        },
        classifyItem: function (item, itemClass, specialClasses) {
            if (("string" == typeof itemClass) && specialClasses && specialClasses[itemClass]) {
                itemClass = specialClasses[itemClass];
            }
            if (Array == item.constructor) {
                for (var i=0; i<item.length; i++) {
                    item[i] = _proton.utils.classifyItem(item[i], itemClass, specialClasses);
                }
                return item;
            }
            if (itemClass instanceof Function) {
                return (item instanceof itemClass)
                    ? item
                    : new itemClass(item);
            }
            _proton.utils.classify(item, itemClass, specialClasses);
            return item;
        },
        classify: function(structure, classes, specialClasses) {
            for(var i in structure) {
                if (structure[i] != null && classes[i]) {
                    switch(structure[i].constructor) {
                        case Object:
                        case Number:
                        case Boolean:
                        case Date:
                        case String:
                        case RegExp:
                        case Array:
                            structure[i] = _proton.utils.classifyItem(structure[i], classes[i], specialClasses);
                            break;
                    }
                }
            }
        },
        
        wrapMethod: function(method, _superMethod) {
            var wrappedMethod =  function () {
                var self = this, originalArguments = arguments;
                var buffer = { _superMethod: this._superMethod, _superCall: this._superCall };
                this._superMethod = _superMethod;
                this._superCall = function() {
                    return _superMethod.apply(self, originalArguments);
                };
                var result =  method.apply(this, arguments);
                this._superMethod = buffer._superMethod;
                this._superCall = buffer._superCall;
                return result;
            }
            wrappedMethod._originalMethod = method;
            return wrappedMethod;
        },

        getMergedPrototypeProperty: function(objectConstructor, propertyName) {
            //building prototype chains
            var currentPrototype = objectConstructor.prototype;
            var chain = [];
            while(currentPrototype) {
                if (currentPrototype.hasOwnProperty(propertyName)) {
                    chain.push(currentPrototype[propertyName]);
                }
                currentPrototype = currentPrototype.constructor.superclass;
            }

            var property = {};
            for (var i=chain.length - 1; i>=0; i--) {
                $.extend(true, property, chain[i]);
            }

            return property;
        },

        jQueryDOM: {
            clone: function(template) {
                return $(template).clone();
            },
            reattach: function(el, newParent) {
                $(el).appendTo(newParent)
            },
            detach: function(el) {
                if (!$("#detached").length) {
                    $("body").append($("<div id='detached' style='display:none;'></div>"))
                }
                this.reattach(el, $("#detached"));
            },
            getContents: function(el) {
                return $(el).contents();
            },
            getDeferred: function() {
                return new $.Deferred();
            }
        }

    }
};

_proton.Root = function() {};
_proton.Root.prototype.satelliteClasses = {};
_proton.Root.prototype.construct = function() {
    this.satellites = {};
};

_proton.Root.extend = function(classInfo) {
    var f = function() {
        return this.construct.apply(this, arguments);
    };
    f.extend = _proton.Root.extend;
    var classParent = this;
    _proton.utils.extend(f, classParent);

    for (var i in classInfo) {
        if (classInfo[i] instanceof Function && f.prototype[i] instanceof Function) {
            f.prototype[i] = _proton.utils.wrapMethod(classInfo[i], f.prototype[i]);
        } else {
            f.prototype[i] = classInfo[i];
        }
    }
    if (f.prototype.satelliteClasses) {
        for (var i in f.prototype.satelliteClasses) {
            if ("object" == typeof(f.prototype.satelliteClasses[i])) {
                var satelliteClassInfo = f.prototype.satelliteClasses[i];
                satelliteClassInfo._p_originalClass = i;

                var satelliteParent = _proton.Root;
                switch(typeof(f.prototype.satelliteClasses[i].parent)) {
                    case "function":
                        satelliteParent = f.prototype.satelliteClasses[i].parent;
                        break;
                    case "string":
                        var parentName = satelliteClassInfo._p_dynamicParent = f.prototype.satelliteClasses[i].parent;
                        if ("function" == typeof(f.prototype.satelliteClasses[parentName])) {
                            satelliteParent = f.prototype.satelliteClasses[parentName];
                        }
                        break;
                    default:
                        if (classParent.prototype.satelliteClasses[i]) {
                            satelliteParent = classParent.prototype.satelliteClasses[i];
                        }
                        break;
                }
                if (!satelliteClassInfo.shortcut) {
                    satelliteClassInfo.shortcut = (satelliteParent.prototype.shortcut && "string" != typeof(f.prototype.satelliteClasses[i].parent)) ? satelliteParent.prototype.shortcut : i;
                }
                f.prototype.satelliteClasses[i] = satelliteParent.extend(satelliteClassInfo);
            }
        }
        for (var i in f.prototype.satelliteClasses) {
            _proton.Root.fixSatellitePrototypeChain(f.prototype, f.prototype.satelliteClasses[i]);
        }
    }
    
    for (var i in f.superclass.satelliteClasses) {
        if (!f.prototype.satelliteClasses[i]) {
            f.prototype.satelliteClasses[i] = f.superclass.satelliteClasses[i];
        }
    }

    return f;
}

_proton.Root.fixSatellitePrototypeChain = function(mainClassPrototype, satelliteConstructor) {
    if (!satelliteConstructor.prototype._p_dynamicParent) return satelliteConstructor;
    if (!mainClassPrototype.satelliteClasses[satelliteConstructor.prototype._p_dynamicParent]) return;

    var currentPrototype = satelliteConstructor.prototype;
    var currentChain = [];
    var _p_dynamicParent = satelliteConstructor.prototype._p_dynamicParent;
    while (currentPrototype) {
        currentChain.push(currentPrototype);
        currentPrototype = currentPrototype.constructor.superclass;
        if (currentPrototype._p_dynamicParent != _p_dynamicParent) {
            currentPrototype = mainClassPrototype.satelliteClasses[_p_dynamicParent].prototype;
            _p_dynamicParent = currentPrototype._p_dynamicParent;
        }
        if (!currentPrototype) {
            break;
        }
        if (!currentPrototype._p_dynamicParent) {
            currentChain.push(currentPrototype);
            break;
        }
    }

    var parent = currentChain[currentChain.length - 1];//mainClassPrototype.satelliteClasses[satelliteConstructor.prototype._p_dynamicParent].prototype;
    for (var i=currentChain.length - 2; i >= 0; i--) {
        var oldPrototype = currentChain[i];
        var f = function() {};
        f.prototype = parent;
        f.superclass = parent;

        var newPrototype = new f();
        newPrototype.constructor = satelliteConstructor;
        for (var j in oldPrototype) {
            if (oldPrototype.hasOwnProperty(j) && j != "constructor") {
                var oldProperty = oldPrototype[j];
                if (oldProperty instanceof Function) {
                    if (oldProperty._originalMethod) {
                        oldProperty = oldProperty._originalMethod;
                    }
                }
                if (newPrototype[j] && (newPrototype[j] instanceof Function) && (oldProperty instanceof Function)) {
                    if (oldProperty == newPrototype[j]._originalMethod) {
                        alert("trouble");
                    }
                    newPrototype[j] = _proton.utils.wrapMethod(oldProperty, newPrototype[j]);
                }   else {
                    newPrototype[j] = oldProperty;
                }
            }
        }

        currentChain[i] = newPrototype;
        parent = newPrototype;
    }

    satelliteConstructor.prototype = currentChain[0];
}

_proton.Root.prototype.getSatellite = function(realm, id) {
    if (!this.satellites[realm]) return null;
    if (null == id) {
        for (var i in this.satellites[realm]) {
            return this.satellites[realm][i];
            break;
        }
    }
    return this.satellites[realm][id] ? this.satellites[realm][id] : null;
};

_proton.Root.prototype.registerSatellite = function(realm, id, satellite) {
    if (!this.satellites[realm]) this.satellites[realm] = {};
    this.satellites[realm][id] = satellite;
};

_proton.Root.prototype.unregisterSatellite = function(realm, id) {
    if (null == id) {
        for (var i in this.satellites[realm]) {
            delete this.satellites[realm][i];
        }
    }   else {
        delete this.satellites[realm][id];
    }
};

_proton.Model = _proton.Root.extend({
    defaults: {},
    dataClassesMap: {},
    construct: function(data) {
        this._superCall();
        $.extend(true, this, {
            subscribers: {},
            subscribersOnAll: [],
            views: {},
            proxy: null
        });

        this.data = {};
        $.extend(true, this.data, _proton.utils.getMergedPrototypeProperty(this.constructor, "defaults"), data);
        this.classify();
    },
    classify: function() {
        _proton.utils.classify(this.data, _proton.utils.getMergedPrototypeProperty(this.constructor, "dataClassesMap"), {
            "self": this.constructor
        });
    },
    subscribe: function (handler, eventName) {
        if (!eventName) {
            this.subscribersOnAll.push(handler);
        }   else {
        if (!this.subscribers[eventName]) this.subscribers[eventName] = [];
            this.subscribers[eventName].push(handler);
        }
    },
    fireEvent: function(eventName, info) {
        if (!info || !info.dontNotifyMyself) {
            this.onOwnEvent(eventName, info);
        }
        if (!info || !info.internal)   {
            if (this.subscribers[eventName]) {
                for (var i=0; i<this.subscribers[eventName].length; i++) {
                    this.subscribers[eventName][i](eventName, info);
                }
            }
            for (var i=0; i<this.subscribersOnAll.length; i++) {
                this.subscribersOnAll[i](eventName, info);
            }
        }
        if (!info || !info.dontNotifySatellite) {
            for (var realm in this.satellites)   {
                for (var i in this.satellites[realm]) {
                    var satellite = this.satellites[realm][i];
                    if ("function" == typeof(satellite.onModelEvent)) {
                        satellite.onModelEvent(eventName, info);
                    }
                }
            }
        }
    },
    getView: function(params) {
        var self = this;
        var view = this.getSatelliteStrong("view", this.satelliteClasses.View, params);
        if (view && !view.getProxy) {
            view.getProxy = function() {
                return self.getProxy.apply(self, arguments);
            }
        }
        return view;
    },
    getProxy: function(params) {
        return this.getSatelliteStrong("proxy", this.satelliteClasses.ProxyWeb, params);
    },
    getSatelliteStrong: function(realm, defaultClass, params) {
        if ("string" == typeof(params)) {
            params = { shortcut: params }
        }
        if (!params) params = {};

        var constructor = defaultClass;
        switch (typeof(params.shortcut)) {
            case "function":
                constructor = params.shortcut;
                break;
            case "string":
                for (var i in this.satelliteClasses) {
                    var satelliteClass = this.satelliteClasses[i];
                    if (params.shortcut == satelliteClass.prototype.shortcut) {
                        constructor = satelliteClass;
                        break;
                    }
                }
                break;
        }
        if (!params.id) params.id = constructor.prototype.shortcut ? constructor.prototype.shortcut : "default";
        if (!params.settings) params.settings = {};

        var satellite = this.getSatellite(realm, params.id);
        if (satellite) return satellite;
        if (params.dontCreate) {
            satellite = this.getSatellite(realm);
            return satellite || null;
        }

        var satellite = this.constructSatellite(realm, constructor, params);
        satellite.id = params.id;
        this.registerSatellite(realm, params.id, satellite);

        return satellite;
    },
    constructSatellite: function(realm, constructor, params) {
        var settings = (params && params.settings) ? params.settings : {};
        var satellite = new constructor({
            modelData: this.data,
            settings: settings
        });
        switch(realm) {
            case "view":
                satellite.modelCallback = _proton.utils.bind(this.onViewEvent, this, satellite);
                break;
            case "proxy":
                satellite.modelCallback = _proton.utils.bind(this.onProxyEvent, this, satellite);
                break;
        }
        return satellite;
    },
    destroyView: function(params) {
        if (!params) params = {};
        params = $.extend(true, {}, { dontCreate: true }, params);
        var view = this.getView(params);
        if (view) {
            view.destroy();
            this.unregisterSatellite("view", view.id);
        }
    },
    onViewEvent: function(view, eventName, info) {
        /*
        switch(eventName) {
            case "get-proxy":
                var proxyParameter = (info) ? info : "web";
                return this.getProxy(proxyParameter);
                break;
        }
        */
    },
    onProxyEvent: function(proxy, eventName, info) {
        switch(eventName) {
            case "apply-loaded-data":
                var dataToApply = info.data;
                if (info.filter) {
                    dataToApply = _proton.utils.filterData(info.data, info.filter);
                }
                $.extend(true, this.data, dataToApply);
                this.classify();
                break;
            case "status-changed":
                var newInfo = $.extend(true, {}, info, { proxy:proxy });
                this.fireEvent("status-changed", newInfo);
                break;
        }
    },
    onOwnEvent: function(eventName, info) {

    },
    destroy: function() {
        if (this.satellites["view"]) {
            for (var i in this.satellites["view"])   {
                if (this.satellites["view"][i].destroy) {
                    this.satellites["view"][i].destroy();
                }
            }
        }
        for (var i in this) {
            delete this[i];
        }
    },

    satelliteClasses: {
        View: {
            template: null,
            environment: _proton.utils.jQueryDOM,
            defaultSettings: {
            },
            onModelEvent: function() {
            },
            prepareEl: function() {
                this.el = this.environment.clone(this.template);
            },
            render: function() {
                if (!this.el) this.prepareEl();
            },
            getEl: function() {
                if (!this.el) {
                    this.render();
                }
                return this.el;
            },
            /*
            getProxy: function(parameter) {
                return this.modelCallback("get-proxy", parameter);
            },
            */
            construct: function (data) {
                var defaultSettings = _p.utils.getMergedPrototypeProperty(this.constructor, "defaultSettings");
                data.settings = $.extend(true, {}, defaultSettings, data.settings);
                $.extend(this, data);
            },
            destroy: function () {
                if (this.el) {
                    this.el.remove();
                }
            }
        },
        Proxy: {
            environment: _proton.utils.jQueryDOM,
            dataMask: null,
            defaultSettings: {},
            STATES: {
                NEW: "new",
                COMPLETE: "complete",
                EMPTY: "empty",
                MODIFIED: "modified"
            },
            LOADING_SAVING_STATES: {
                IDLE: "idle",
                PROGRESS: "progress",
                ERROR: "error"
            },
            CHANGES: {
                LOADING_STARTED: "loading-started",
                LOADING_ERROR: "loading-error",
                LOADING_SUCCESS: "loading-success",
                REFRESH_STARTED: "refresh-started",
                REFRESH_ERROR: "refresh-error",
                REFRESH_SUCCESS: "refresh-success",
                SAVING_STARTED: "saving-started",
                SAVING_ERROR: "saving-error",
                SAVING_SUCCESS: "saving-success",
                REMOVING_STARTED: "REMOVing-started",
                REMOVING_ERROR: "REMOVing-error",
                REMOVING_SUCCESS: "REMOVing-success",
                MODIFIED: "modified",
                UNMODIFIED: "unmodified"
            },

            construct: function(data) {
                this._superCall();
                data.settings = $.extend(true, {}, this.defaultSettings, data.settings);
                $.extend(this, data);
                this.status = {
                    state: this.detectState(),
                    loadingState: this.LOADING_SAVING_STATES.IDLE,
                    refreshState: this.LOADING_SAVING_STATES.IDLE,
                    savingState: this.LOADING_SAVING_STATES.IDLE,
                    removingState: this.LOADING_SAVING_STATES.IDLE
                };
                this.changeset = 0;
                this.beingSavedChangeset = null;
            },

            detectState: function() {
                return this.STATES.EMPTY;
            },

            modify: function() {
                this.changeset++;
                if (this.status.state != this.STATES.MODIFIED && this.status.state != this.STATES.NEW) {
                    this.beforeModifiedState = this.status.state;
                    this.status.state = this.STATES.MODIFIED;
                    this.statusChanged(this.CHANGES.MODIFIED);
                }
            },

            unmodify: function() {
                if (this.STATES.MODIFIED == this.status.state) {
                    this.status.state = (this.beforeModifiedState)
                        ? this.beforeModifiedState
                        : this.detectState();
                    this.statusChanged(this.CHANGES.UNMODIFIED);
                }
            },

            statusChanged: function(reason) {
                this.modelCallback("status-changed", {
                    reason: reason,
                    status: this.status
                });
            },

            isLoadNeeded: function() {
                var result =
                    (this.status.loadingState != this.LOADING_SAVING_STATES.PROGRESS) &&
                    (this.status.state == this.STATES.EMPTY);

                return result;
            },
            isSaveNeeded: function() {
                var result =
                    (this.status.savingState != this.LOADING_SAVING_STATES.PROGRESS) &&
                    (this.status.state == this.STATES.NEW || this.status.state == this.STATES.MODIFIED);

                return result;
            },

            load: function(key) {
                var deferred = this.environment.getDeferred();
                this.status.loadingState = this.LOADING_SAVING_STATES.PROGRESS;
                this.statusChanged(this.CHANGES.LOADING_STARTED);

                return deferred;
            },
            refresh: function(key, filter) {
                var deferred = this.environment.getDeferred();
                this.status.refreshState = this.LOADING_SAVING_STATES.PROGRESS;
                this.statusChanged(this.CHANGES.REFRESH_STARTED);

                return deferred;
            },
            save: function(key) {
                var deferred = this.environment.getDeferred();

                this.beingSavedChangeset = this.changeset;
                this.status.savingState = this.LOADING_SAVING_STATES.PROGRESS;
                this.statusChanged(this.CHANGES.SAVING_STARTED);
                
                return deferred;
            },
            remove: function(key) {
                var deferred = this.environment.getDeferred();

                this.status.removingState = this.LOADING_SAVING_STATES.PROGRESS;
                this.statusChanged(this.CHANGES.REMOVING_STARTED);

                return deferred;
            },

            getLoadingKey: function() {
                return null;
            },
            getSavingKey: function() {
                return null;
            },

            onLoadingSuccess: function() {
                this.status.loadingState = this.LOADING_SAVING_STATES.IDLE;
                this.statusChanged(this.CHANGES.LOADING_SUCCESS);
            },
            onRefreshSuccess: function() {
                this.status.refreshState = this.LOADING_SAVING_STATES.IDLE;
                this.statusChanged(this.CHANGES.REFRESH_SUCCESS);
            },
            onSavingSuccess: function() {
                this.status.savingState = this.LOADING_SAVING_STATES.IDLE;
                if (this.beingSavedChangeset == this.changeset) {
                    this.status.state = this.STATES.COMPLETE;
                }   else {
                    this.status.state = this.STATES.MODIFIED;
                }
                this.statusChanged(this.CHANGES.SAVING_SUCCESS);
            },
            onRemovingSuccess: function() {
                this.status.removingState = this.LOADING_SAVING_STATES.IDLE;
                this.status.state = this.STATES.NEW;
                this.statusChanged(this.CHANGES.REMOVING_SUCCESS);
            },

            onLoadingError: function() {
                this.status.loadingState = this.LOADING_SAVING_STATES.ERROR;
                this.statusChanged(this.CHANGES.LOADING_ERROR);
            },
            onRefreshError: function() {
                this.status.refreshState = this.LOADING_SAVING_STATES.ERROR;
                this.statusChanged(this.CHANGES.REFRESH_ERROR);
            },
            onSavingError: function() {
                this.status.savingState = this.LOADING_SAVING_STATES.ERROR;
                this.statusChanged(this.CHANGES.SAVING_ERROR);
            },
            onRemovingError: function() {
                this.status.removingState = this.LOADING_SAVING_STATES.ERROR;
                this.statusChanged(this.CHANGES.REMOVING_ERROR);
            },

            applyLoadedData: function(data, filter) {
                this.modelCallback("apply-loaded-data", {
                    data: data,
                    filter: filter
                });
                if (this.STATES.EMPTY == this.status.state) {
                    this.status.state = this.STATES.COMPLETE;
                }
            },
            getSavingPreparedData: function() {
                var data = _proton.utils.filterData(this.modelData, this.dataMask, this.shortcut)
                return data;
            }
        },

        ProxyWeb: {
            parent: "Proxy",
            shortcut: "web",

            refresh: function(userKey, filter) {
                var deferred = this._superCall();
                this.doLoading({
                    userKey: userKey,
                    filter: filter,
                    success: "onRefreshSuccess",
                    error: "onRefreshError",
                    deferred: deferred
                });
                return deferred;
            },

            load: function(userKey) {
                var deferred = this._superCall();
                this.doLoading({
                    userKey: userKey,
                    success: "onLoadingSuccess",
                    error: "onLoadingError",
                    deferred: deferred
                });
                return deferred;
            },

            doLoading: function(settings) {
                var self = this;
                var deferred = settings.deferred || this.environment.getDeferred();
                try {
                    var key = this.getLoadingKey();
                    if (settings.userKey) {
                        $.extend(true, key, settings.userKey);
                    }

                    var ajaxArgument = $.extend({}, {
                        type: "GET",
                        dataType: "text"
                    }, key, {
                        success: function(data, textStatus, xhr) {
                            try {
                                self.applyLoadedData(JSON.parse(data), settings.filter);
                                self[settings.success]();
                                deferred.resolve();
                            } catch(e) {
                                self[settings.error](e);
                                deferred.reject();
                            }
                        },
                        error: function(xhr, textStatus, httpStatus) {
                            self[settings.error](new Error(textStatus));
                            deferred.reject();
                        },
                        complete: null
                    });
                    $.ajax(ajaxArgument);
                } catch(e) {
                    self[settings.error](e);
                    deferred.reject();
                }
                return deferred;
            },

            save: function(key) {
                var deferred = this._superCall();
                var self = this;
                try {
                    if (!key) key = this.getSavingKey();
                    var ajaxArgument = $.extend({}, {
                        type: "PUT",
                        dataType: "text",
                        data: this.getSavingPreparedData()
                    }, key, {
                        success: function(data, textStatus, xhr) {
                            try {
                                self.checkSavingResult(data, textStatus, xhr);
                                self.onSavingSuccess();
                                deferred.resolve();
                            } catch(e) {
                                self.onSavingError(e);
                                deferred.reject();
                            }
                        },
                        error: function(xhr, textStatus, httpStatus) {
                            self.onSavingError(new Error(textStatus));
                            deferred.reject();
                        },
                        complete: null
                    });
                    $.ajax(ajaxArgument);
                } catch(e) {
                    self.onSavingError(e);
                    deferred.reject();
                }
                return deferred;
            },

            checkSavingResult: function(data, textStatus, xhr) {
                if ("success" != textStatus) throw new Error("Bad status: " + textStatus);
            },

            remove: function(key) {
                var deferred = this._superCall();
                var self = this;
                try {
                    if (!key) key = this.getSavingKey();
                    
                    var ajaxArgument = $.extend({}, {
                        type: "DELETE",
                        dataType: "text"
                    }, { url: key.url }, {
                        success: function(data, textStatus, xhr) {
                            try {
                                self.onRemovingSuccess();
                                deferred.resolve();
                            } catch(e) {
                                self.onRemovingError(e);
                                deferred.reject();
                            }
                        },
                        error: function(xhr, textStatus, httpStatus) {
                            self.onRemovingError(new Error(textStatus));
                            deferred.reject();
                        },
                        complete: null
                    });
                    $.ajax(ajaxArgument);
                } catch(e) {
                    self.onRemovingError(e);
                    deferred.reject();
                }
                return deferred;
            }
        },

        ProxyStorage: {
            parent: "Proxy",
            shortcut: "storage",

            defaultSettings: {
                asynchronousCalls: false
            },

            load: function(key) {
                var deferred = this._superCall();
                var self = this;
                try {
                    if (undefined == window.localStorage) throw new Error("localStorage is not supported");
                    if (!key) key = this.getLoadingKey();
                    if ("string" != typeof(key)) throw new Error("proxyStorage key must be a string");

                    if (this.settings.asynchronousCalls) {
                        setTimeout(function() {
                            self.loadWorker(key, deferred);
                        }, 1);
                    }   else {
                        this.loadWorker(key, deferred);
                    }
                } catch(e) {
                    this.onLoadingError(e);
                    deferred.reject();
                }
                return deferred;
            },

            loadWorker: function(key, deferred) {
                try {
                    var data = localStorage.getItem(key);
                    if (null == data) {
                        throw new Error("requested data not found in localStorage");
                    }
                    this.applyLoadedData(JSON.parse(data));
                    this.onLoadingSuccess();
                    deferred.resolve();
                } catch(e) {
                    this.onLoadingError(e);
                    deferred.reject();
                }
            },

            save: function(key) {
                var deferred = this._superCall();
                var self = this;
                try {
                    if (undefined == window.localStorage) throw new Error("localStorage is not supported");
                    if (!key) key = this.getSavingKey();
                    if ("string" != typeof(key)) throw new Error("proxyStorage key must be a string");
                    var data = this.getSavingPreparedData();
                    if ("object" != typeof(data)) throw new Error("proxyStorage data must be a JSON-compatible object");
                    data = JSON.stringify(data);

                    if (this.settings.asynchronousCalls) {
                        setTimeout(function() {
                            self.saveWorker(key, data, deferred);
                        }, 1);
                    }   else {
                        this.saveWorker(key, data, deferred);
                    }
                } catch(e) {
                    this.onSavingError(e);
                    deferred.reject();
                }
                return deferred;
            },

            saveWorker: function(key, data, deferred) {
                try {
                    localStorage.setItem(key, data);
                    this.checkSavingResult(key, data);
                    this.onSavingSuccess();
                    deferred.resolve();
                } catch(e) {
                    this.onSavingError(e);
                    deferred.reject();
                }
            },

            checkSavingResult: function(key, data) {
                var restoredData = localStorage.getItem(key);
                if (data != restoredData) throw new Error("localStorage saving verification failure");
            },

            remove: function(key) {
                var deferred = this._superCall();
                var self = this;
                try {
                    if (undefined == window.localStorage) throw new Error("localStorage is not supported");
                    if (!key) key = this.getSavingKey();
                    if ("string" != typeof(key)) throw new Error("proxyStorage key must be a string");

                    if (this.settings.asynchronousCalls) {
                        setTimeout(function() {
                            self.removeWorker(key, deferred);
                        }, 1);
                    }   else {
                        this.removeWorker(key, deferred);
                    }
                } catch(e) {
                    this.onRemovingError(e);
                    deferred.reject();
                }
                return deferred;
            },

            removeWorker: function(key, deferred) {
                try {
                    localStorage.removeItem(key);
                    this.checkRemovingResult(key, data);
                    this.onRemovingSuccess();
                    deferred.resolve();
                } catch(e) {
                    this.onRemovingError(e);
                    deferred.reject();
                }
            }
        },
        ProxyCookie: {
            parent: "Proxy",
            shortcut: "cookie",
            
            defaultSettings: {
                asynchronousCalls: false
            },

            load: function(key) {
                var deferred = this._superCall();
                var self = this;
                try {
                    if (!key) key = this.getLoadingKey();
                    if ("string" == typeof(key)) {
                        key = {name: key};
                    }
                    if ("string" != typeof(key.name)) throw new Error("cookieProxy key shouldn't be empty");
                    if (!key.name) throw new Error("cookieProxy key shouldn't be empty");

                    if (this.settings.asynchronousCalls) {
                        setTimeout(function() {
                            self.loadWorker(key, deferred);
                        }, 1);
                    }   else {
                        this.loadWorker(key, deferred);
                    }
                } catch(e) {
                    self.onLoadingError(e);
                    deferred.reject();
                }
                return deferred;
            },

            loadWorker: function(key, deferred) {
                try {
                    var data = null;
                    var start = document.cookie.indexOf(key.name+"=");
                    if (-1 == start) {
                        throw new Error("not-found");
                    }
                    var end = document.cookie.indexOf(";", start);
                    if (-1 == end) {
                        end = document.cookie.length;
                    }
                    data = document.cookie.substring(start, end);
                    data = this.decodeData(data);

                    this.applyLoadedData(JSON.parse(data));
                    this.onLoadingSuccess();
                    deferred.resolve();
                } catch(e) {
                    this.onLoadingError(e);
                    deferred.reject();
                }
            },

            save: function(userKey) {
                var deferred = this._superCall();
                var self = this;
                
                try {
                    if ("string" == typeof(userKey)) {
                        userKey = {name: userKey};
                    }

                    var key = this.getSavingKey();
                    if ("string" == typeof(key)) {
                        key = {name:key};
                    }

                    if (!key && !userKey) throw new Error("ProxyCookie saving key should not be empty");
                    if (!key) key = {};
                    if (userKey) {
                        $.extend(true, key, userKey);
                    }
                    if ("string" != typeof(key.name)) throw new Error("ProxyCookie key.name must be a string");
                    if (!key.name) throw new Error("ProxyCookie key.name should not be empty");

                    var data = this.getSavingPreparedData();
                    if ("object" != typeof(data)) throw new Error("ProxyCookie data must be a JSON-compatible object");

                    if (this.settings.asynchronousCalls) {
                        setTimeout(function() {
                            self.saveWorker(key, data, deferred);
                        }, 1);
                    }   else {
                        this.saveWorker(key, data, deferred);
                    }
                } catch(e) {
                    self.onSavingError(e);
                    deferred.reject();
                }
                return deferred;
            },

            saveWorker: function(key, data, deferred) {
                try {
                    data = this.encodeData(JSON.stringify(data));

                    document.cookie = key.name + "=" + data +
                        ((key.expires) ? "; expires=" + key.expires : "") +
                        ((key.path) ? "; path=" + key.path : "") +
                        ((key.domain) ? "; domain=" + key.domain : "") +
                        ((key.secure) ? "; secure" : "");

                    this.checkSavingResult(key.name, data);
                    this.onSavingSuccess();
                    deferred.resolve();
                } catch(e) {
                    this.onSavingError(e);
                    deferred.reject();
                }
            },

            checkSavingResult: function(key, data) {
                var start = document.cookie.indexOf(key+"=");
                if (-1 == start) throw new Error("ProxyCookie saving verification failure: not found");

                var end = document.cookie.indexOf(";", start);
                if (-1 == end) {
                    end = document.cookie.length;
                }
                restoredData = document.cookie.substring(start, end);
                if (data != restoredData) throw new Error("ProxyCookie saving verification failure");
            },

            encodeData: function(data) {
                data = encodeURIComponent(data);
                data = data
                    .replace("%7B", "{")
                    .replace("%7D", "}")
                    .replace("%5B", "[")
                    .replace("%5D", "]")
                    .replace("%2C", ",")
                    .replace("%3A", ":")
                    .replace("%2B", "+")
                    .replace("%2F", "/");
                
                return data;
            },

            decodeData: function(data) {
                return decodeURIComponent(data);
            },

            remove: function(userKey) {
                var deferred = this._superCall();
                var self = this;

                try {
                    if ("string" == typeof(userKey)) {
                        userKey = {name: userKey};
                    }

                    var key = this.getSavingKey();
                    if ("string" == typeof(key)) {
                        key = {name:key};
                    }

                    if (!key && !userKey) throw new Error("ProxyCookie saving key should not be empty");
                    if (!key) key = {};
                    if (userKey) {
                        $.extend(true, key, userKey);
                    }
                    if ("string" != typeof(key.name)) throw new Error("ProxyCookie key.name must be a string");
                    if (!key.name) throw new Error("ProxyCookie key.name should not be empty");

                    if (this.settings.asynchronousCalls) {
                        setTimeout(function() {
                            self.removeWorker(key, deferred);
                        }, 1);
                    }   else {
                        this.removeWorker(key, deferred);
                    }
                } catch(e) {
                    self.onRemovingError(e);
                    deferred.reject();
                }
                return deferred;
            },

            removeWorker: function(key, deferred) {
                try {
                    document.cookie = key.name + "=" +
                        "; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/";

                    this.onRemovingSuccess();
                    deferred.resolve();
                } catch(e) {
                    this.onRemovingError(e);
                    deferred.reject();
                }
            }
        }
    }
});

_proton.Loadable = _proton.Model.extend(
/**
 * Describes the Loadable class - a class which View implements rendering logic for loading states
 * @lends Loadable
 */
{
    defaults: {
        defaultProxyParameter: "web"
    },

    construct: function() {
        this._superCall();
        var proxy = this.getProxy(this.data.defaultProxyParameter);
    },

    getProxy: function(parameter) {
        if (!parameter) parameter = this.data.defaultProxyParameter;
        return this._superMethod(parameter);
    },

    loadIfNeeded: function(getProxyParameter) {
        var proxy = this.getProxy(getProxyParameter);
        if (proxy.isLoadNeeded()) {
            return proxy.load();
        }
        return true;
    },

    getView: function() {
        this.loadIfNeeded();
        return this._superCall();
    },

    onViewEvent: function(view, eventName, info) {
        switch(eventName) {
            case "modified":
                this.getProxy().modify();
                break;
            default:
                return this._superCall();
                break;
        }
    },

    satelliteClasses: { 
        View: {
            loadingTemplate: null,
            errorTemplate: null,

            render: function() {
                this._superCall();
                this.unmodify();

                var proxy = this.getProxy();
                switch(proxy.status.state) {
                    case proxy.STATES.EMPTY:
                        this.contentsBlock = this.environment.getContents(this.el);
                        this.loadingBlock = this.environment.clone(this.loadingTemplate);
                        this.errorBlock = this.environment.clone(this.errorTemplate);

                        this.environment.detach(this.contentsBlock);

                        this.updateLoader(this.getProxy(), null);
                        break;
                    case proxy.STATES.NEW:
                    case proxy.STATES.MODIFIED:
                        this.modify();
                        this.renderLoaded();
                        break;
                    default:
                        this.renderLoaded();
                        break;
                }

            },

            updateLoader: function(proxy, reason) {
                this.environment.detach(this.loadingBlock);
                this.environment.detach(this.errorBlock);

                if (proxy.STATES.EMPTY == proxy.status.state)  {
                    switch(proxy.status.loadingState)  {
                        case proxy.LOADING_SAVING_STATES.PROGRESS:
                            this.environment.reattach(this.loadingBlock, this.el);
                            break;
                        case proxy.LOADING_SAVING_STATES.ERROR:
                            this.environment.reattach(this.errorBlock, this.el);
                            break;
                    }
                } else {
                    this.destroyLoader();
                    if (!this.loadedRendered) {
                        this.renderLoaded();
                    }
                }
            },

            destroyLoader: function() {
                this.environment.detach(this.loadingBlock);
                this.environment.detach(this.errorBlock);
                this.environment.reattach(this.contentsBlock, this.el);
            },

            renderLoaded: function() {
                this.loadedRendered = true;
            },

            modify: function() {
            },

            unmodify: function() {
            },

            onModelEvent: function(eventName, info) {
                switch(eventName) {
                    case "status-changed":
                        var proxy = info.proxy;
                        switch(info.reason) {
                            case proxy.CHANGES.LOADING_SUCCESS:
                                this.updateLoader(proxy, info.reason);
                                break;
                            case proxy.CHANGES.LOADING_ERROR:
                                this.updateLoader(proxy, info.reason);
                                break;
                            case proxy.CHANGES.MODIFIED:
                                this.modify();
                                break;
                            case proxy.CHANGES.UNMODIFIED:
                                this.unmodify();
                                break;
                            case proxy.CHANGES.SAVING_SUCCESS:
                                this.unmodify();
                                break;
                            default:
                                return this._superCall();
                                break;
                        }
                        break;
                    default:
                        this._superCall();
                        break;
                }
            }
        }
    }
});